package com.iwhalecloud.citybrain.flow.platform.manager.application.utils;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

import com.alibaba.fastjson.JSONObject;

import com.fasterxml.jackson.databind.JsonNode;
import com.iwhalecloud.citybrain.flow.platform.manager.application.exceptions.InvalidProcessModelException;
import com.iwhalecloud.citybrain.flow.platform.manager.domain.enums.OneVoteVetoTypeEnum;
import com.iwhalecloud.citybrain.flow.platform.manager.domain.model.ModelEdgeVo;
import com.iwhalecloud.citybrain.flow.platform.manager.domain.model.ModelEditVo;
import com.iwhalecloud.citybrain.flow.platform.manager.domain.model.ModelGroupVo;
import com.iwhalecloud.citybrain.flow.platform.manager.domain.model.ModelNodeVo;
import com.iwhalecloud.citybrain.flow.platform.manager.domain.model.NodeVo;
import org.springframework.util.CollectionUtils;

/**
 * 将前端编辑器编辑的流程涉及子流程的部分进行重新构建
 * 1. 找到名为groups的字段，并填充flowable标准字段
 * 2. 找到所有子流程中的开始节点和结束节点
 * 3. 对于指向子流程开始节点的边，更新它们的target为子流程id
 * 4. 对于从子流程结束节点流出的边，更新他们的source为子流程的id
 * 5. 将group当成特殊的node，加入nodes列表
 *
 * 前端请求：
 *                        子流程
 * start1----[--->start2--->userTask1--->end2--]---->end1
 *
 * 后端收到，改为：
 *
 * start1--->[ start2--->userTask2--->end2]---->end1
 *
 *
 * @author lich
 * @date 2021/11/1
 */
public class SubProcessTransformer {

    private List<ModelGroupVo> groups;

    private List<ModelNodeVo> nodes;

    private List<ModelEdgeVo> edges;

    private List<ModelNodeVo> subProcessStartNodes;

    private List<ModelNodeVo> subProcessEndNodes;

    // key: edge的source id， value: edge本身
    private Map<String, List<ModelEdgeVo>> sourceEdgeMap;

    // key: edge的target id，value: edge本身
    private Map<String, List<ModelEdgeVo>> targetEdgeMap;

    // 非子流程节点节点
    private Set<String> outerNodes;

    // 内部的边集合
    Map<String, List<ModelEdgeVo>> innerEdges;

    public SubProcessTransformer(ModelEditVo modelEditVo) {
        this.groups = modelEditVo.getModelJson().getGroups();
        this.nodes = modelEditVo.getModelJson().getNodes();
        this.edges = modelEditVo.getModelJson().getEdges();

        this.outerNodes = nodes.stream()
            .filter(nodeVo -> nodeVo.getParent() == null)
            .map(n -> n.getId())
            .collect(Collectors.toSet());

        this.innerEdges = new HashMap<>();

        this.subProcessStartNodes = retrieveSubProcessStartNodes(nodes);
        this.subProcessEndNodes = retrieveSubProcessEndNodes(nodes);

        initSourceTargetEdgesMap();
    }

    public Set<String> transform() {
        initGroups();

        changeEdgeTargetPoint2StartNodes();
        changeEdgeSource2EndNodes();
        changeEdgeForInner();
        addGroupsToNodes();

        return groupIds();
    }

    private void addGroupsToNodes() {
        this.nodes.addAll(this.groups);
    }

    private void initGroups() {
        for (ModelGroupVo groupVo : this.groups) {
            NodeVo nodeVo = groupVo.getModel();
            JSONObject stencilObject = new JSONObject();
            stencilObject.put("id", "SubProcess");
            nodeVo.setStencil(stencilObject);

            JSONObject propertiesObject;
            if (nodeVo.getProperties() == null) {
                propertiesObject = new JSONObject();
            } else {
                propertiesObject = JSONObject.parseObject(JSONObject.toJSONString(nodeVo.getProperties()));
            }
            propertiesObject.put("activitytype", "SubProcess");
            propertiesObject.put("asynchronousdefinition", false);
            propertiesObject.put("exclusivedefinition", true);
            putDefaultIfNotPresent(propertiesObject, "multiinstance_type", "Parallel");
            putDefaultIfNotPresent(propertiesObject, "multiinstance_collection", "${loopNum}");
            putDefaultIfNotPresent(propertiesObject, "multiinstance_variable", "elementVar");
            putDefaultIfNotPresent(propertiesObject, "multiinstance_condition", "${nrOfPassInstances/nrOfInstances>=1}");
            putDefaultIfNotPresent(propertiesObject, "oneVoteVetoType",
                Integer.valueOf(OneVoteVetoTypeEnum.ONE_VOTE_VETO_EVERY_ONE.getCode()).intValue());
            nodeVo.setProperties(propertiesObject);
        }
    }

    private static void putDefaultIfNotPresent(JSONObject jsonObject, String key, String value) {
        if (!jsonObject.containsKey(key)) {
            jsonObject.put(key, value);
        }
    }

    private static void putDefaultIfNotPresent(JSONObject jsonObject, String key, int value) {
        if (!jsonObject.containsKey(key)) {
            jsonObject.put(key, value);
        }
    }

    private void changeEdgeForInner() {
        Set<String> startIds = this.subProcessStartNodes.stream().map(ModelNodeVo::getId).collect(Collectors.toSet());
        Set<String> endIds = this.subProcessEndNodes.stream().map(ModelNodeVo::getId).collect(Collectors.toSet());

        List<ModelNodeVo> innerNodes = nodes.stream()
            .filter(n -> n.getParent() != null && !startIds.contains(n.getId()) && !endIds.contains(n.getId()))
            .collect(Collectors.toList());

        for (ModelNodeVo nodeVo : innerNodes) {
            if (sourceEdgeMap.containsKey(nodeVo.getId())) {
                collectInnerEdges(nodeVo.getParent(), sourceEdgeMap.get(nodeVo.getId()));
            }
            if (targetEdgeMap.containsKey(nodeVo.getId())) {
                collectInnerEdges(nodeVo.getParent(), targetEdgeMap.get(nodeVo.getId()));
            }
        }
    }

    private void collectInnerEdges(String parent, List<ModelEdgeVo> innerOnes) {
        if (CollectionUtils.isEmpty(innerOnes)) {
            return;
        }
        innerOnes.forEach(e -> verifyInnerEdge(parent, e));
        if (!innerEdges.containsKey(parent)) {
            innerEdges.put(parent, new ArrayList<>());
        }
        innerOnes.stream().forEach(e -> e.setParent(parent));
        innerEdges.get(parent).addAll(innerOnes);
    }

    private boolean isOutterNode(String nodeId) {
       return this.outerNodes.contains(nodeId);
    }

    private void verifyInnerEdge(String parent, ModelEdgeVo edgeVo) {
        if (isOutterNode(edgeVo.getSource())) {
            throw new InvalidProcessModelException(
                String.format("子流程外部不能指向内部：边(id:%s, label:%s)属于子流程(id:%s)，"
                        + "但是其source指向的节点(id:%s)不属于子流程",
                    edgeVo.getId(), edgeVo.getLabel(), parent, edgeVo.getSource()));
        }
        if (isOutterNode(edgeVo.getTarget())) {
            throw new InvalidProcessModelException(
                String.format("子流程内部不能指向外部：id为(id:%s, label:%s)的边属于子流程(id:%s)，"
                        + "但是其target指向的节点(id:%s)不属于子流程",
                    edgeVo.getId(), edgeVo.getLabel(), parent, edgeVo.getTarget()));
        }
    }

    private void changeEdgeTargetPoint2StartNodes() {
        for (ModelNodeVo nodeVo: this.subProcessStartNodes) {
            String parent = nodeVo.getParent();
            List<ModelEdgeVo> targetAtMe = targetEdgeMap.get(nodeVo.getId());
            if (!CollectionUtils.isEmpty(targetAtMe)) {
                //对于指向子流程开始节点的边，我们让他们target指向该group（子流程）
                for (ModelEdgeVo edge : targetAtMe) {
                    changeEdgeTargetTo(edge, parent);
                }
            }
            List<ModelEdgeVo> sourceFromMe = sourceEdgeMap.get(nodeVo.getId());
            collectInnerEdges(parent, sourceFromMe);
        }
    }

    private void changeEdgeSource2EndNodes() {
        for (ModelNodeVo nodeVo: this.subProcessEndNodes) {
            String parent = nodeVo.getParent();
            List<ModelEdgeVo> sourceFromMe = sourceEdgeMap.get(nodeVo.getId());
            if (!CollectionUtils.isEmpty(sourceFromMe)) {
                //对于source是子流程结束节点的边，我们让他们source为该group（子流程）
                for(ModelEdgeVo edge : sourceFromMe) {
                    changeEdgeSourceTo(edge, parent);
                }
            }
            List<ModelEdgeVo> targetAtMe = targetEdgeMap.get(nodeVo.getId());
            collectInnerEdges(parent, targetAtMe);
        }
    }

    private static void changeEdgeTargetTo(ModelEdgeVo edgeVo, String newTarget) {
        edgeVo.setGroupTarget(newTarget);
    }

    private static void changeEdgeSourceTo(ModelEdgeVo edgeVo, String newSource) {
        edgeVo.setGroupSource(newSource);
    }

    private Set<String> groupIds() {
        return groups.stream()
            .map(g -> g.getId())
            .collect(Collectors.toSet());
    }

    private void initSourceTargetEdgesMap() {
        this.sourceEdgeMap = new HashMap<>();
        this.targetEdgeMap = new HashMap<>();
        for (ModelEdgeVo edge : edges) {
            if (!sourceEdgeMap.containsKey(edge.getSource())) {
                sourceEdgeMap.put(edge.getSource(), new ArrayList<>());
            }
            sourceEdgeMap.get(edge.getSource()).add(edge);

            if (!targetEdgeMap.containsKey(edge.getTarget())) {
                targetEdgeMap.put(edge.getTarget(), new ArrayList<>());
            }
            targetEdgeMap.get(edge.getTarget()).add(edge);
        }
    }

    private static List<ModelNodeVo> retrieveSubProcessStartNodes(List<ModelNodeVo> nodes) {
        return nodes.stream()
            .filter(nodeVo -> (nodeVo.getParent() != null) && isStartEventNode(nodeVo))
            .collect(Collectors.toList());
    }

    private static List<ModelNodeVo> retrieveSubProcessEndNodes(List<ModelNodeVo> nodes) {
        return nodes.stream()
            .filter(nodeVo -> (nodeVo.getParent() != null) && isEndEventNode(nodeVo))
            .collect(Collectors.toList());
    }

    private static boolean isStartEventNode(ModelNodeVo nodeVo) {
        Object stencil = nodeVo.getModel().getStencil();
        String stencilType = readFieldForInObjectAsText(stencil, "id");
        boolean isStartEvent = false;
        if (stencilType != null) {
             isStartEvent = (("StartNoneEvent".equals(stencilType) || "GroupTaskStart".equals(stencilType)));
        }
        if (!isStartEvent) {
            isStartEvent ="GroupTaskStart".equals(nodeVo.getShape());
        }
        // 前端missing stencil.id
        if (isStartEvent) {
            JSONObject stencilObject = new JSONObject();
            stencilObject.put("id", "StartNoneEvent");
            nodeVo.getModel().setStencil(stencilObject);
        }

        return isStartEvent;
    }

    private static boolean isEndEventNode(ModelNodeVo nodeVo) {
        Object stencil = nodeVo.getModel().getStencil();
        String stencilType = readFieldForInObjectAsText(stencil, "id");
        boolean isEndEvent = false;
        if (stencilType != null) {
             isEndEvent = ("EndNoneEvent".equals(stencilType) || "GroupTaskEnd".equals(stencilType));
        }
        if (!isEndEvent) {
            isEndEvent = "GroupTaskEnd".equals(nodeVo.getShape());
        }
        // 前端missing stencil.id
        if (isEndEvent) {
            JSONObject stencilObject = new JSONObject();
            stencilObject.put("id", "EndNoneEvent");
            nodeVo.getModel().setStencil(stencilObject);
        }

        return isEndEvent;
    }

    private static String readFieldForInObjectAsText(Object o, String field) {
        JsonNode jsonNode = JsonUtils.readTree(JsonUtils.toJson(o));
        jsonNode = jsonNode.get(field);
        if (jsonNode != null) {
            return jsonNode.asText();
        }
        return null;
    }
}
